/**
 * 
 */
package co.droidforum.foodx.ws;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import co.droidforum.foodx.R;
import co.droidforum.foodx.application.AplicationContext;

/**
 * @author andres.alzate
 *
 */
public class ServiceCommunication {
	
	//Variables de comunicacion con el web service
	private static String urlService;
	
	public static final String BODY_RESPONSE = "body_response";
	
	private static ServiceCommunication serviceCommunication;
	private static RestTemplate restTemplate;

	private static ServiceCommunication serviceCommunicationUrl;
	
	final static HostnameVerifier DO_NOT_VERIFY = new HostnameVerifier() {
		public boolean verify(String hostname, SSLSession session) {
			return true;
		}
	};
	
	private ServiceCommunication(String url){
		urlService = url;
		restTemplate = new RestTemplate();
	}
	
	private ServiceCommunication(){
		//Se obtiene la URL del servicio a la que se va a conectar
		urlService = AplicationContext.getValueStringResource(R.string.urlServiceGP);
		
		//Se crea el rest template
		createRestTemplate();
	}
	
	
	
	/**
	 * Obtiene la instancia del servico de comunicacion para
	 * iniciar la conexion y comunicacion con los web services
	 * 
	 * @param urlService Url del servico al que se desea comunicar
	 * @return
	 */
	public static ServiceCommunication getInstance(){
		if(serviceCommunication == null){
			serviceCommunication = new ServiceCommunication();
		}
		
		return serviceCommunication;
	}
	
	/**
	 * Esta funcion permite ejecutar una peticion al web service que se indica
	 * por medio de la URL enviandole los datos que estan contenidos en el objeto
	 * que se envia como peticion y transformado en notacion JSON, sin tener ninguna
	 * informacion de respuesta para dicha peticion.
	 * 
	 * Esta funcion solo se utiliza cuando se necesita enviar informacion al servidor.
	 * 
	 * @param uri Ruta relativa del web service que se desea llamar
	 * @param httpMethod Metodo por el cual se va a solicitar la peticion (GET, POST, PUT, DELETE)
	 * @param requestObject Objeto que contiene la informacion que va a ser enviada al web service
	 * 						y transformada en notacion JSON
	 * @param headerValues Valores que van a ser enviados en la cabecera de la peticion. El nombre
	 * 					   del valor del encabezado debe concordar con el nombre que es conocido
	 * 					   por el servidor
	 * @throws RemoteConnectionServiceException, RemoteBusinessServiceException
	 */
	public void executeRequestDataJSON(String uri, HttpMethod httpMethod, Object requestObject, Map<String, String> headerValues) throws RemoteBusinessServiceException, RemoteConnectionServiceException {
		//Se asignan los datos del encabezado
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.setContentType(new MediaType("application", "json"));
		putHeaderValues(requestHeaders, headerValues);
		
		//Se registran las peticiones que se van a realizar al web service
		HttpEntity<Object> requestEntity = new HttpEntity<Object>(requestObject, requestHeaders);
		
		//URL del web service que se quiere llamar
		String url = urlService + uri;
		
		//Se realiza la peticion al web service
		restTemplate.exchange(url, httpMethod, requestEntity, null);
	}
	
	/**
	 * Esta funcion permite ejecutar una peticion al web service que se indica
	 * por medio de la URL enviandole los datos que estan contenidos en el objeto
	 * que se envia como peticion y transformado en notacion JSON, obteniendo
	 * como respuesta una imagen y datos adicionales que estan contenidos en el header
	 * de la respuesta.
	 * 
	 * Esta funcion solo se utiliza cuando se necesita enviar datos al web service y este
	 * devuelva una imagen con informacion adicional
	 * 
	 * @param uri Ruta relativa del web service que se desea llamar
	 * @param httpMethod Metodo por el cual se va a solicitar la peticion (GET, POST, PUT, DELETE)
	 * @param requestObject Objeto que contiene la informacion que va a ser enviada al web service
	 * 						y transformada en notacion JSON
	 * @param instanceResponse Clase de la instancia que se va a crear con la informacion de la respuesta
	 * 						   obtenida en el web service
	 * @param headerValues Valores que van a ser enviados en la cabecera de la peticion. El nombre
	 * 					   del valor del encabezado debe concordar con el nombre que es conocido
	 * 					   por el servidor
	 * @return
	 * @throws RemoteConnectionServiceException, RemoteBusinessServiceException
	 */
	public Map<String, Object> executeResponseDataImage(String uri, HttpMethod httpMethod, Object requestObject, Map<String, String> headerValues) throws RemoteBusinessServiceException, RemoteConnectionServiceException {
		//Se asignan los datos del encabezado
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.setContentType(new MediaType("application","json"));
		putHeaderValues(requestHeaders, headerValues);
		
		//Se registran las peticiones que se van a realizar al web service
		HttpEntity<Object> requestEntity = new HttpEntity<Object>(requestObject, requestHeaders);
		
		//URL del web service que se quiere llamar
		String url = urlService + uri;

		//Se realiza la peticion al web service
		ResponseEntity<byte[]> responseEntity = restTemplate.exchange(url, httpMethod, requestEntity, byte[].class);
		Map<String, Object> data = getHeaderData(responseEntity);
		
		byte[] image = responseEntity.getBody();
		data.put(BODY_RESPONSE, image);
		
		return data;
	}
	
	/**
	 * Esta funcion permite ejecutar una peticion al web service que se indica
	 * por medio de la URL enviandole los datos que estan contenidos en el objeto
	 * que se envia como peticion y transformado en notacion JSON, obteniendo
	 * la instancia de la clase que se ha pasado como objeto de respuesta.
	 * 
	 * Esta funcion solo se utiliza cuando se necesita enviar datos al web service y este
	 * devuelva un objeto o una lista de objetos como respuesta.
	 * 
	 * @param uri Ruta relativa del web service que se desea llamar
	 * @param httpMethod Metodo por el cual se va a solicitar la peticion (GET, POST, PUT, DELETE)
	 * @param requestObject Objeto que contiene la informacion que va a ser enviada al web service
	 * 						y transformada en notacion JSON
	 * @param instanceResponse Clase de la instancia que se va a crear con la informacion de la respuesta
	 * 						   obtenida en el web service
	 * @param headerValues Valores que van a ser enviados en la cabecera de la peticion. El nombre
	 * 					   del valor del encabezado debe concordar con el nombre que es conocido
	 * 					   por el servidor
	 * @return
	 * @throws RemoteConnectionServiceException, RemoteBusinessServiceException
	 */
	public <T> T sendResponseDataJSON(String uri, HttpMethod httpMethod, Object requestObject, Class<T> instanceResponse, Map<String, String> headerValues) throws RemoteBusinessServiceException, RemoteConnectionServiceException {
		//Se asignan los datos del encabezado
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.setContentType(new MediaType("application","json"));
		putHeaderValues(requestHeaders, headerValues);
		
		//Se registran las peticiones que se van a realizar al web service
		HttpEntity<Object> requestEntity = new HttpEntity<Object>(requestObject, requestHeaders);
		
		//URL del web service que se quiere llamar
		String url = urlService + uri;

		//Se realiza la peticion al web service
		ResponseEntity<T> responseEntity = restTemplate.exchange(url, httpMethod, requestEntity, instanceResponse);
		T respuesta = responseEntity.getBody();
		
		return respuesta;
	}
	
	/**
	 * Esta funcion permite ejecutar una peticion al web service que se indica
	 * por medio de la URL enviandole los datos que estan contenidos en el objeto
	 * que se envia como peticion y transformado en notacion JSON, obteniendo
	 * como respuesta un texto plano (Una instancia de tipo String).
	 * 
	 * Esta funcion solo se utiliza cuando se necesita enviar datos al web service y este
	 * devuelva un texto plano como respuesta.
	 * 
	 * @param uri Ruta relativa del web service que se desea llamar
	 * @param httpMethod Metodo por el cual se va a solicitar la peticion (GET, POST, PUT, DELETE)
	 * @param requestObject Objeto que contiene la informacion que va a ser enviada al web service
	 * 						y transformada en notacion JSON
	 * @param headerValues Valores que van a ser enviados en la cabecera de la peticion. El nombre
	 * 					   del valor del encabezado debe concordar con el nombre que es conocido
	 * 					   por el servidor
	 * @return
	 * @throws RemoteConnectionServiceException, RemoteBusinessServiceException
	 */
	public String sendResponseDataTEXT(String uri, HttpMethod httpMethod, Object requestObject, Map<String, String> headerValues) throws RemoteBusinessServiceException, RemoteConnectionServiceException {
		//Se asignan los datos del encabezado
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.setContentType(new MediaType("application","json"));
		putHeaderValues(requestHeaders, headerValues);
		
		//Se registran las peticiones que se van a realizar al web service
		HttpEntity<Object> requestEntity = new HttpEntity<Object>(requestObject, requestHeaders);
		
		//URL del web service que se quiere llamar
		String url = urlService + uri;

		//Se realiza la peticion al web service
		ResponseEntity<String> responseEntity = restTemplate.exchange(url, httpMethod, requestEntity, String.class);
		String respuesta = responseEntity.getBody();
		
		return respuesta;
	}
	
	/**
	 * Esta funcion permite ejecutar una peticion al web service que se indica
	 * por medio de la URL, obteniendo la instancia de la clase que se ha pasado
	 * como objeto de respuesta.
	 * 
	 * Esta funcion solo se utiliza cuando se necesita consultar datos que no necesitan
	 * que se envie informacion para su consulta.
	 * 
	 * @param uri Ruta relativa del web service que se desea llamar
	 * @param httpMethod Metodo por el cual se va a solicitar la peticion (GET, POST, PUT, DELETE)
	 * @param instanceResponse Clase de la instancia que se va a crear con la informacion de la respuesta
	 * 						   obtenida en el web service
	 * @param headerValues Valores que van a ser enviados en la cabecera de la peticion. El nombre
	 * 					   del valor del encabezado debe concordar con el nombre que es conocido
	 * 					   por el servidor
	 * @return
	 * @throws RemoteConnectionServiceException, RemoteBusinessServiceException
	 */
	public <T> T responseDataJSON(String uri, HttpMethod httpMethod, Class<T> instanceResponse, Map<String, String> headerValues) throws RemoteBusinessServiceException, RemoteConnectionServiceException {
		//Se asignan los datos del encabezado
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.setContentType(new MediaType("application","json"));
		putHeaderValues(requestHeaders, headerValues);
		
		//Se registran las peticiones que se van a realizar al web service
		HttpEntity<Object> requestEntity = new HttpEntity<Object>(requestHeaders);
		
		//URL del web service que se quiere llamar
		String url = urlService + uri;

		//Se realiza la peticion al web service
		ResponseEntity<T> responseEntity = restTemplate.exchange(url, httpMethod, requestEntity, instanceResponse);
		T respuesta = responseEntity.getBody();
		
		return respuesta;
	}
	
	public <T> T responseDataJSON(String uri, HttpMethod httpMethod, Class<T> instanceResponse) throws RemoteBusinessServiceException, RemoteConnectionServiceException {
		return responseDataJSON(uri, httpMethod, instanceResponse, null);
	}	
	
	/**
	 * Esta funcion permite ejecutar una peticion al web service que se indica
	 * por medio de la URL y enviando la informacion de la imagen al servidor.
	 * 
	 * Esta funcion solo se utiliza cuando se necesita enviar una imagen para ser
	 * almacenada en un sistema externo.
	 * 
	 * @param uri Ruta relativa del web service que se desea llamar
	 * @param httpMethod Metodo por el cual se va a solicitar la peticion (GET, POST, PUT, DELETE)
	 * @param bytes Informacion de la imagen representada en bytes
	 * @param imageType Tipo de imagen que se desea enviar (JPG, GIF, PNG, entre otras. Estos tipos de imagen deben ser las que
	 * 					estan especificadas internacionalmente)
	 * @param headerValues Valores que van a ser enviados en la cabecera de la peticion. El nombre
	 * 					   del valor del encabezado debe concordar con el nombre que es conocido
	 * 					   por el servidor
	 * @throws RemoteBusinessServiceException
	 * @throws RemoteConnectionServiceException
	 */
	public void sendImageData(String uri, HttpMethod httpMethod, byte[] bytes, String imageType, Map<String, String> headerValues) throws RemoteBusinessServiceException, RemoteConnectionServiceException {
		//Se asignan los datos del encabezado
		HttpHeaders requestHeaders = new HttpHeaders();
		requestHeaders.setContentType(new MediaType("image", imageType));
		putHeaderValues(requestHeaders, headerValues);
		
		//Se registran las peticiones que se van a realizar al web service
		HttpEntity<Object> requestEntity = new HttpEntity<Object>(bytes, requestHeaders);
		
		//URL del web service que se quiere llamar
		String url = urlService + uri;

		//Se realiza la peticion al web service
		restTemplate.exchange(url, httpMethod, requestEntity, null);
	}
	
	/**
	 * Permite poner los valores que van a ser enviador por el header
	 * al HttpHeader
	 * 
	 * @param httpHeader
	 * @param headerValues
	 */
	private void putHeaderValues(HttpHeaders httpHeaders, Map<String, String> headerValues) {
		if(headerValues != null) {
			Set<Entry<String, String>> headerEntries = headerValues.entrySet();
			for(Entry<String, String> entry : headerEntries){
				String headerName = entry.getKey();
				String headerValue = entry.getValue();
				
				httpHeaders.add(headerName, headerValue);
			}
		}
	}
	
	/**
	 * Obtiene los datos que se han recibido por medio del
	 * encabezado de la respuesta
	 * 
	 * @param responseEntity
	 * @return
	 */
	private <T> Map<String, Object> getHeaderData(ResponseEntity<T> responseEntity){
		Map<String, Object> data = new HashMap<String, Object>();
		HttpHeaders httpHeaders = responseEntity.getHeaders();
		
		Set<Entry<String, List<String>>> headerEntries = httpHeaders.entrySet();
		for(Entry<String, List<String>> entry : headerEntries){
			String headerName = entry.getKey();
			String headerValue = ((List<String>) entry.getValue()).get(0);
			
			data.put(headerName, headerValue);
		}
		
		return data;
	}
	
	/**
	 * Crea las configuraciones del rest template para que se pueda
	 * dar una buena comunicacion con los web service
	 */
	private void createRestTemplate(){
		restTemplate = new RestTemplate();
		
		//Se asigna el manejador de errores 
		restTemplate.setErrorHandler(new RemoteResponseErrorHandler());
		
		//Se asigna el el timeout de la espera de conexion con el web service
		int timeOut = Integer.parseInt(AplicationContext.getValueStringResource(R.string.timeOutWS));
		HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory = (HttpComponentsClientHttpRequestFactory) restTemplate.getRequestFactory();
		httpComponentsClientHttpRequestFactory.setReadTimeout(timeOut);
		
		//Se agrega el interceptor que va a manejar la respuesta del web service
		ClientHttpRequestInterceptor[] interceptors = {	new ClientHttpRequestInterceptorHandler() };
		restTemplate.setInterceptors(interceptors);
	}
	
	public static ServiceCommunication getInstance(String url){
		if(serviceCommunicationUrl == null)
			serviceCommunicationUrl = new ServiceCommunication(url);
		return serviceCommunicationUrl;
	}
}